Pitfalls of Demonstration

I came across an article called "Pitfalls of Safe Rust", which I just couldn't pass by, because of such demonstrations.

First of all, I see the listed examples with overflow:

// 1
fn calculate_total(price: u32, quantity: u32) -> u32 {
    price * quantity  // Could overflow!
}

//2
fn main() {
    let x: u8 = 2;
    let y: u8 = 128;
    let z = x * y;
}

Here firstly, u8 is an 8-bit integer that takes values from 0 to 255. Secondly, we multiply 2 by 128 and get 256. Of course, this will lead to overflow.

In the provided example, the values 2 and 128 are valid because the u8 type is allocated for them. What would we do otherwise, specifying a larger type for the output:

fn main() {
    let x: u8 = 2;
    let y: u8 = 128;
    let z: u16 = x as u16 * y as u16;  // all good bro no worries
}

Or even so:

fn no_worries(x: u8, y: u8) -> u16 {
    (x * y).into()
}

fn main() {
    let x: u8 = 2;
    let y: u8 = 128;
    no_worries(x, y);
}

But that's not all. Let's go back to the example with the calculate_total() function and expand it for clarity. In my case, the calculate_total() function example didn't provide clarity until I started expanding and experimenting:

fn main() {
    calculate_total(8);
}

fn calculate_total(value: u8) -> u8 {
    let multiplier: u8 = 200;
    value * multiplier
}

Here emerges an interesting picture. The compiler wasn't able to warn me about the potential problem before runtime. As a result, a panic occurs:

thread 'main' panicked at src/main.rs:7:5:
attempt to multiply with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

This reveals a significant gap in analysis capabilities. The compiler doesn't see that we're calling the function with the specific value 8 because it analyzes the function definition independently of how it's used.

However, Rust provides tools for working with such cases. For our case with the calculate_total() function, this would be suitable:

 /// Checked integer multiplication. Computes `self * rhs`, returning
        /// `None` if overflow occurred.
        ///
        /// # Examples
        ///
        /// Basic usage:
        ///
        /// ```
        #[doc = concat!("assert_eq!(5", stringify!($SelfT), ".checked_mul(1), Some(5));")]
        #[doc = concat!("assert_eq!(", stringify!($SelfT), "::MAX.checked_mul(2), None);")]
        /// ```
        #[stable(feature = "rust1", since = "1.0.0")]
        #[rustc_const_stable(feature = "const_checked_int_methods", since = "1.47.0")]
        #[must_use = "this returns the result of the operation, \
                      without modifying the original"]
        #[inline]
        pub const fn checked_mul(self, rhs: Self) -> Option<Self> {
            let (a, b) = self.overflowing_mul(rhs);
            if intrinsics::unlikely(b) { None } else { Some(a) }
        }

How can we apply the solution? In this case, the checked_mul() function pattern is combined with our calculate_total() function. So, we take the problematic part:

fn main() {
    calculate_total(8);
}

fn calculate_total(value: u8) -> u8 {
    let multiplier: u8 = 200;
    value * multiplier
}

And we treat it with the checked_mul():

fn main() {
    match calculate_total(8) {
        Ok(result) => println!("Result: {}", result),
        Err(err) => println!("Error: {}", err),
    }
}

fn calculate_total(value: u8) -> Result<u8, &'static str> {
    let multiplier: u8 = 200;
    value.checked_mul(multiplier).ok_or("Overflow occurred")
}

After which, instead of panic, we get the following output:

Error: Overflow occurred

My verdict is that there are no universal solutions, and therenever will be. It's practically impossible to cover all cases and handle everything.